package org.axed.user.client;

import com.google.gwt.core.client.GWT;

/**
 * A pointer information. Other than the anchor this is not integrated 
 * into the document, and thus does not automatically adapt to document
 * changes like the anchor.
 */
public class AxedPointer {
	/**
	 * Return values for isInHTMLTag()
	 */
	public final static int TAG_NO    = 0;
	public final static int TAG_BEGIN = 1;
	public final static int TAG_END   = 2;
	public final static int TAG_ONE   = 3;

	static Coords coords1 = new Coords();

	/**
	 * buffer for varioud routines.
	 */
	static AxedPointer bp1 = new AxedPointer(null);

	public int line = 0;
	public int col  = 0;

	/**
	 * optional to be used, last left position
	 * (e.g. for cursor up/down movements)
	 */
	public int retainLeft = 0;

	/**
	 * last col position (same as retainLeft but for non-visual mode)
	 */
	public int retainCol = 0;

	/** 
	 * the AxedArea this points into
	 */
	protected AxedArea axed;

	/**
	 * Constructor.
	 */
	public AxedPointer(AxedArea axed) {
		this.axed = axed;
	}

	/**
	 * Returns the character this pointer points at.
	 * or -1 if at Eol
	 */
	public char getChar() {
		if (isEol()) {
			return (char) -1;
		}
		return axed.getChar(this);
	}

	/**
	 * Returns the character relativ to the pointer.
	 * or -1 if at Eol
	 */
	public char getChar(int rel) {
		int ll = axed.getLineLength(line);
		if (rel >= 0) {
			if (col + rel >= ll) {
				return (char) -1;
			}
		} else {
			if (rel > col) {
				return (char) -1;
			}
		}
		return axed.getChar(line, col + rel);
	}

	/**
	 * returns a textual representation for
	 * the return values of isInHtmlTag()
	 * for debugging.
	 */
	public static String getIsTagString(int t) {
		switch (t) {
		case TAG_NO    : return "no";
		case TAG_BEGIN : return "begin";
		case TAG_END   : return "end";
		case TAG_ONE   : return "one";
		default:
		return "internal error in getIsTagString()";
		}
	}

	/**
	 * Returns a short debug string.
	 */
	public String getDebugText() {
		return line + "/" + col;
	}

	/**
	 * Returns -1 if this pointer if before p.
	 *          0 if it is equal
	 *         +1 if its after
	 */
	public int compare(AxedPointer p) {
		assert axed == p.axed;
		if (line < p.line) {
			return -1;
		}
		if (line == p.line) {
			if (col < p.col) {
				return -1;
			}
			if (col == p.col) {
				return 0;
			}
			return +1;
		}
		return +1;
	}

	/**
	 * Returns true when to pointers point to the same place
	 */
	public boolean equals(AxedPointer p) {
		assert axed == p.axed;
		return line == p.line && col == p.col;
	}

	/**
	 * sets this pointer to "not set".
	 */
	public void invalidize() {
		line = col = -1;
	}

	/**
	 * Returns true when at 0/0.
	 */
	public boolean isPos1() {
		return line == 0 && col == 0;
	}

	/**
	 * Returns true when at end of line.
	 */
	public boolean isEol() {
		return col >= axed.getLineLength(line);
	}

	/**
	 * Returns true when at end of file.
	 */
	public boolean isEof() {
		return isLastLine() && isEol();
	}

	/**
	 * Returns true when in last line.
	 */
	public boolean isLastLine() {
		return line + 1 >= axed.getLineCount();
	}

	/**
	 * Returns true when inside an HTML tag.
	 *
	 * @return TAG_NO when outside
	 *         TAG_BEGIN when in <foo>
	 *         TAG_END   whe  in </foo/>
	 *         TAG_ONE   when in <foo/>
	 */
	public int isInHTMLTag() {
		int c = col;
		int ll = axed.getLineLength(line);
		if (c >= ll) {
			return TAG_NO;
		}
		char ch = axed.getChar(line, c);
		if (ch == '>') {			// stands already on end
			if (c == 0) {
				return TAG_NO;
			}
			c--;
			ch = axed.getChar(line, c); 
		}
		// work toward '<'
		while (ch != '<') {
			if (ch == '>') { 	// encounters an end
				return TAG_NO;
			}
			c--;
			if (c < 0) {
				return TAG_NO;
			}
			ch = axed.getChar(line, c); 
		}
		if (c + 1 >= ll) {
			return TAG_NO;
		}
		char ch2 = axed.getChar(line, c + 1);
		if (ch2 == '/') {
			return TAG_END;
		}
		// looks if this is a onecell specifier like <bla/>
		ch2 = '<';
		while (ch != '>') {
			ch2 = ch;
			c++;
			if (c >= ll) {
				return TAG_NO;
			}
			ch = axed.getChar(line, c);
		}
		if (ch2 == '/') {
			return TAG_ONE;
		} else {
			return TAG_BEGIN;
		}
	}

	/**
	 * Moves the pointer by the size of an intervall.
	 */
	public void moveBy(AxedInterval iv) {
		String was = getDebugText();
		line += Math.abs(iv.p2.line - iv.p1.line);
		int lc = axed.getLineCount() - 1;
		if (line > lc) {
			line = lc;
			col = axed.getLineLength(line);
			return;
		}
		int ll = axed.getLineLength(line);
		if (iv.isOneLined()) {
			col += Math.abs(iv.p2.col - iv.p1.col);
		} else {
			if (iv.p1.compare(iv.p2) <= 0) {
				col = iv.p2.col;
			} else {
				col = iv.p1.col;
			}
		}
		if (col > ll) {
			col = ll;
		}
		GWT.log(was + " : " + getDebugText() + "(" + axed.getLineCount() + ", " + ll + ")", null);
	}

	/**
	 * Moves the pointer count characters to the left.
	 * (if moved retainLeft will be reset to -1)
	 *
	 * @param lineWrap, when true do jump to next document lines.
	 *
	 * @return total chars moved
	 */
	public int moveLeft(int count, boolean lineWrap) {
		int ret = 0;
		while (count > 0) {
			if (isPos1()) {
				return ret;
			} 
			if (col >= 1) {
				if (count > col) {
					ret   += col;
					count -= col;
					col=0;
					retainLeft = retainCol = -1;
					continue;
				}
				col -= count;
				ret += count;
				retainLeft = retainCol = -1;
				return ret;
			}
			assert col == 0;
			if (!lineWrap) {
				return ret;
			}
			line--;
			ret++;
			count--;
			retainLeft = retainCol = -1;
			col = axed.getLineLength(line);
		}
		return ret;
	}

	/**
	 * Moves the pointer count characters to the right.
	 * (if moved retainLeft will be reset to -1)
	 *
	 * @param lineWrap, when true do jump to next document lines.
	 *
	 * @return total chars moved
	 */
	public int moveRight(int count, boolean lineWrap) {
		int ret = 0;
		while (count > 0) {
			int ll = axed.getLineLength(line);
			if (col >= ll) {
				if (isEof()) {
					// already at end of document
					return ret;
				} 
				if (!lineWrap) {
					return ret;
				}
				col = 0;
				line++;
				count--;
				ret++;
				retainLeft = retainCol = -1;
				continue;
			}
			int lr = ll - col;
			if (count > lr) {
				col += lr;
				count -= lr;
				ret += lr;
				retainLeft = retainCol = -1;
				if (!lineWrap) {
					return ret;
				}
				line++;
			} else {
				col += count;
				ret += count;
				retainLeft = retainCol = -1;
				return ret;
			}
		}
		return ret;
	}

	/**
	 * Moves the pointer count lines down.
	 * (if visual cursors current left position will be stored
	 *  in retain left).
	 *
	 * @param count   the number of line to move up.
	 * @param visual  when true count lines on screen
	 *
	 * @return total lines moved (visual or phsyical depending on mode)
	 */
	public int moveDown(int count, boolean visual) {
		int asw = 0;
		if (!visual) {
			// this is the more trivial case.
			// in non visual mode left coords are also ignored. 
			line += count;
			asw += count;
			int lc = axed.getLineCount() - 1;
			if (line > lc) {
				asw -= lc - line; 
				line = lc;
			}
			if (retainCol < 0) {
				retainCol = col;
			}
			int ll = axed.getLineLength(line);
			col = retainCol < ll ? retainCol : ll;
			return asw;
		}

		// handle visual mode.
		while (count > 0) {
			axed.getCoords(this, coords1);
			if (retainLeft < 0) {
				retainLeft = coords1.left;
			}
		
			if (axed.getLineBottom(line) <= coords1.top + coords1.height) {
				// move a real line downward
	 			if (isLastLine()) {
					return asw;
				}
				col = axed.findColPos(line + 1, axed.getLineTop(line + 1), 
				                      retainLeft, 0, true);
				line++;
				asw++;
				count--;
			} else {
				// stay in this real-line
				col = axed.findColPos(line, coords1.top + axed.lineHeight(), 
				                      retainLeft, col, true);
				asw++;
				count--;
			}
		}
		return asw;
	}

	/**
	 * Moves the pointer count lines up.
	 * (if visual cursors current left position will be stored
	 *  in retain left).
	 *
	 * @param count   the number of line to move up.
	 * @param visual  when true count lines on screen
	 *
	 * @return total lines moved (visual or phsyical depending on mode)
	 */
	public int moveUp(int count, boolean visual) {
		int asw = 0;
		if (!visual) {
			// this is the more trivial case.
			// in non visual mode left coords are also ignored. 
			line -= count;
			asw += count;
			if (line < 0) {
				asw += line; // reduce the answer by the negative lines overshot
				line = 0;
			}
			if (retainCol < 0) {
				retainCol = col;
			}
			int ll = axed.getLineLength(line);
			col = retainCol < ll ? retainCol : ll;
			return asw;
		}

		// handle visual mode.
		while (count > 0) {
			axed.getCoords(this, coords1);
			if (retainLeft < 0) {
				retainLeft = coords1.left;
			}
		
			if (axed.getLineTop(line) >= coords1.top) {
				// move a real line upward
				if (line == 0) {
					return asw;
				}
				line--;
				asw++;
				count--;
				col = axed.findColPos(line, axed.getLineBottom(line) - axed.lineHeight(), 
				                      retainLeft, axed.getLineLength(line), false);
			} else {
				// stay in this real-line
				col = axed.findColPos(line, coords1.top - axed.lineHeight(), 
				                      retainLeft, col, false);
				asw++;
				count--;
			}
		}
		return asw;
	}
	/**
	 * Moves the pointer to line end.
	 *
	 * @param visual  when true move the visual line end.
	 *                when false move to eol
	 */
	public void moveLineEnd(boolean visual) {
		retainLeft = -1;
		if (!visual) {
			col = axed.getLineLength(line);
			return;
		}

		axed.getCoords(this, coords1);
		//TODO shortcut when at bottom to eol.
		col = axed.findWrap(line, coords1.top, col, true);
	}

	/**
	 * Move this pointer to end of file(document).
	 */
	public void moveEof() {
		line = axed.getLineCount() - 1;
		col  = axed.getLineLength(line);
	}

	/**
	 * Moves the pointer to line start.
	 *
	 * @param visual  when true move the visual line start.
	 *                when false move to col 0
	 */
	public void moveLineStart(boolean visual) {
		retainLeft = -1;
		if (!visual) {
			col = 0;
			return;
		}

		axed.getCoords(this, coords1);
		if (coords1.top == axed.getLineTop(line)) { 
			col = 0;
		} else {
			col = axed.findWrap(line, coords1.top  - axed.lineHeight(), col, true) + 1;
		}
	}

	/**
	 * moves the pointer to the next word. 
	 *
	 * @param count    number of words to move.
	 * @param peculiar if true seperate nondigit/letter chars 
	 *                 if false consider every nonspace as word.
	 *
	 * @param start  true move to start of word, false end of it. 
	 *
	 * @return chars moved 
	 */
	public int moveNextWord(int count, boolean peculiar, boolean start) {
		int asw = 0;
		while (count > 0) {
			// skip the white space
			if (!start) {
				while (isEol() || (charClass() == 0)) {
					if (isEof()) {
						return asw;
					}
					asw += moveRight(1, true);
				}
			}
			int cc = isEol() ? 0 : charClass(); // the chartype the pointer was at.	
			while (isEol() || (charClass() == 0)) {
				if (isEof()) {
					return asw;
				}
				asw += moveRight(1, true);
			}
			// skip to end of current word
			while (!isEol() && (
			          ( peculiar && charClass() == cc) ||
			          (!peculiar && charClass() != 0))) {
				asw += moveRight(1, true);
				if (isEof()) {
					return asw;
				}
			}
			// skip the white space
			if (start) {
				while (isEol() || (charClass() == 0)) {
					if (isEof()) {
						return asw;
					}
					asw += moveRight(1, true);
				}
			}
			count--;
		}
		return asw;
	}

	/**
	 * moves the pointer to the next counted char in this line containt ch
	 *
	 * @param count ... if positive move forward
	 * @param lineWrap ... if true go over lines.
	 *
	 * @return chars moved
	 */
	public int moveToChar(int count, char ch, boolean lineWrap) {
		int ll = axed.getLineLength(line);
		int asw = 0;
		bp1.set(this);
		if (count > 0) {
			while (count > 0) {
				while (bp1.getChar() != ch) {
					if (bp1.moveRight(1, lineWrap) == 0) {
						return -1;
					}
					asw++;
				}
				count--;
			}
			this.set(bp1);
			return asw;
		} else {
			count = -count;
			while (count > 0) {
				while (bp1.getChar() != ch) {
					if (bp1.moveLeft(1, lineWrap) == 0) {
						return -1;
					}
					asw++;
				}
				count--;
			}
			this.set(bp1);
			return asw;
		}
	}

	/**
	 * moves the pointer to the previous tag.
	 *
	 * @param count numbe of levels to go up.
	 *
	 * @return the tag name
	 */
	public String movePrevTag(int count) {
		bp1.set(this);

		switch (isInHTMLTag()) {
		case TAG_NO    : 
			break;
		case TAG_BEGIN : 
		case TAG_ONE   : 
			moveToChar(1, '>', false);
			moveRight(1, true);
			break;
		case TAG_END :
			moveToChar(-1, '<', false);
			moveLeft(1, true);
			break;
		}

		int level = 0;
		while (count > 0) {
			if (moveToChar(-1, '>', true) < 0) {
				this.set(bp1);
				return null;
			}
			moveLeft(1, false);
			if (getChar() == '/') {
				// its an one-line tag.
				continue;
			}

			moveToChar(-1, '<', false);

			if (getChar(+1) == '/') {
				// its an end tag.
				level++; 
				continue;
			}

			// its a begin tag
			if (level > 0) {
				level--;
				continue;
			}
			
			count--;
		}
		// this is tag searched for.
		int l = 1;
		char ch = getChar(l); 
		while (ch != '>' && ch != ' ') {
			l++;
			ch = getChar(l); 
		}
		return axed.getText(line).substring(col + 1, col + l);
	}

	/**
	 * moves the pointer to a matching end tag.
	 * The cursor has to be on the start &gt; tag.
	 *
	 * @param s the tag
	 *
	 * @return true if found.
	 */
	public boolean moveEndTag(String s) {
		bp1.set(this);
		
		int level = -1;
		do {
			if (moveToChar(1, '<', true) < 0) {
				this.set(bp1);
				return false;
			}
			moveRight(1, false);
			boolean end = false;
			if (getChar() == '/') {
				end = true;
				moveRight(1, false); 
			}
			int l = 1;
			char ch = getChar(l); 
			while (ch != '>' && ch != ' ') {
				l++;
				ch = getChar(l); 
			}
			String t = axed.getText(line).substring(col, col + l);
			if (moveToChar(1, '>', false) < 0) {
				continue;
			}
			if (getChar(-1) == '/') {
				// its an one-line tag.
				continue;
			}
			moveRight(1, true);
			if (!t.equals(s)) {
				continue;
			}
			if (!end) {
				level++;
				continue;
			}
			level--;
			continue;
		} while (level >= 0);
		return true;
	}

	/**
	 * moves the pointer to the previous word.
	 *
	 * @param count    number of words to move.
	 * @param peculiar if true seperate nondigit/letter chars 
	 *                 if false consider every nonspace as word.
	 *
	 * @param start  true move to start of word, false end of it. 
	 *
	 * @return chars moved 
	 */
	public int movePrevWord(int count, boolean peculiar, boolean start) {
		int asw = 0;
		while (count > 0) {
			// skip the white space
			if (start) {
				while (isEol() || (charPrevClass() == 0)) {
					if (isPos1()) {
						return asw;
					}
					asw += moveLeft(1, true);
				}
			}
			int cc = isEol() ? 0 : charPrevClass(); // the chartype the pointer was at.	
			while (isEol() || (charPrevClass() == 0)) {
				if (isPos1()) {
					return asw;
				}
				asw += moveLeft(1, true);
			}
			// skip to end of current word
			while (!isEol() && (
			          ( peculiar && charPrevClass() == cc) ||
			          (!peculiar && charPrevClass() != 0))) {
				asw += moveLeft(1, true);
				if (isPos1()) {
					return asw;
				}
			}
			// skip the white space
			if (!start) {
				while (isEol() || (charPrevClass() == 0)) {
					if (isPos1()) {
						return asw;
					}
					asw += moveLeft(1, true);
				}
			}
			count--;
		}
		return asw;
	}
	
	/**
	 * moves the pointer to the start or end of the current word. 
	 *
	 * @param peculiar if true seperate nondigit/letter chars 
	 *                 if false consider every nonspace as word.
	 *
	 * @param start  true move to start of word, false end of it. 
	 *
	 * @return chars moved 
	 */
	public int moveThisWord(boolean peculiar, boolean start) {
		int asw = 0;
		int ll = axed.getLineLength(line); 
		if (ll == 0) {
			return 0;
		}
		if (isEol()) {
			if (start) {
				moveLeft(1, false);
			} else {
				return 0;
			}
		}
		int cc = charClass(); // the chartype the pointer was at.
		if (!start) {
			while (!isEol() && 
				   (( peculiar && (charClass() == cc)) ||
				    (!peculiar && ((charClass() != 0) == (cc != 0)))
				   )) {
				asw += moveRight(1, false);
			}
		} else {
			while (col > 0 && 
				   (( peculiar && (charPrevClass() == cc)) ||
				    (!peculiar && ((charPrevClass() != 0) == (cc != 0)))
				   )) {
				asw += moveLeft(1, false);
			}
		}
		return asw;
	}

	/**
	 * Moves the pointer to the next or previous dot followed by a space/eol.
	 */
	public int moveSentence(int count, boolean space) {
		int asw = 0;
		boolean forward = count > 0;
		if (count < 0) {
			count = -count;
		}
		while (count > 0) {
			if (isLineLimiter()) {
				moveLeft(1, false);
			}
			if (forward) {
				while (true) {
					if (!isEol() && isLineLimiter() &&
					   (col + 1 >= axed.getLineLength(line) || Character.isSpace(axed.getChar(line, col + 1)))) {
						if (!isEol()) {
							moveRight(1, false);
						}
						if (space) {
							while (!isEol() && Character.isSpace(axed.getChar(this))) {
								moveRight(1, false);
							}
						}
						break;
					}
					if (col == 0 && isEol()) { // break on empty line
						moveLeft(1, true);
						break;
					}
					if (moveRight(1, true) == 0) { // move right
						break;					   // and break on eof.
					}
				}
			} else {
				// backward
				while (true) {
					if (!isEol() && isLineLimiter() &&
					   (col + 1 >= axed.getLineLength(line) || Character.isSpace(axed.getChar(line, col + 1)))) {
						moveRight(1, false);
						if (!space) {
							moveRight(1, false);
							while (!isEol() && Character.isSpace(axed.getChar(this))) {
								moveRight(1, false);
							}
						}
						break;
					}
					if (col == 0 && isEol()) { // break on empty line
						moveRight(1, true);
						break;
					}
					if (moveLeft(1, true) == 0) { // move left
						break;					  // and break on start of file.
					}
				}
			}
			count--;
		}
		return asw;
	}

	/**
	 * Moves the pointer count pages up or down.
	 * @param count   the number of pages to move up (negative) down (positive)
	 */
	public void movePages(int count) {
		axed.getCoords(this, coords1);
		int i = 0;
		int y = coords1.top + axed.getClientHeight() * count; 
		y = y - (y % axed.lineHeight()) + (count < 0 ? axed.lineHeight() : 0);
		int lc = axed.getLineCount();
		for (i = 0; i < lc; i++) {
			if (axed.getLineBottom(i) > y) {
				break;
			}
		}
		if (i >= lc) {
			i = lc - 1;
		}
		col = axed.findColPos(i, y, retainLeft >= 0 ? retainLeft : coords1.left, 0, true);
		line = i;
		if (col < 0) {
			col = 0; //TODO???
		}
	}

	/**
	 * Move this pointer to pos1.
	 */
	public void movePos1() {
		line = 0;
		col = 0;
	}

	/**
	 * Set this pointer equal to p.
	 */
	public void set(AxedPointer p) {
		axed       = p.axed;
		line       = p.line;
		col        = p.col;
		retainLeft = p.retainLeft;
		retainCol  = p.retainCol;
	}

	/**
	 * Set this pointer line and col;
	 */
	public void set(int line, int col) {
		this.line       = line;
		this.col        = col;
	}

	/**
	 * Returns true when this pointer points on a '.', '!' or '?'.
	 */
	public boolean isLineLimiter() {
		char c = getChar();
		return c == '.' || c == '!' || c == '?';
	}

	/**
	 * returns 0 for current char == space 
	 *         1 for letter/digit
	 *         2 fo anythingElse
	 *
	 * TODO: move peculiar logic into charClass
	 */
	private int charClass() {
		char c = axed.getChar(this);
		if (Character.isSpace(c)) {
			return 0;
		}
		if (Character.isLetterOrDigit(c)) {
			return 1;
		}
		return 2;
	}

	/**
	 * returns 0 for previos char == space or start of line.
	 *         1 for letter/digit
	 *         2 fo anythingElse
	 */
	private int charPrevClass() {
		if (col == 0) {
			return 0;
		}
		char c = axed.getChar(line, col - 1);
		if (Character.isSpace(c)) {
			return 0;
		}
		if (Character.isLetterOrDigit(c)) {
			return 1;
		}
		return 2;
	}


}

